﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace System.Threading
{
  /// <summary>
  /// Performs work on a background thread when an event occurs.
  /// </summary>
  /// <remarks>
  /// A worker loops until Stop is called.
  /// 
  /// A worker will check to see if Stop is called or if CanPerformWork is true every _internalInterval.
  /// 
  /// Alternatively, setting the waitHandle will invoke PerformWork on the background thread.
  /// </remarks>
  public abstract class Worker : System.Threading.Workers.IWorker
  {
    #region Fields
    private Thread[] _workerThreads;
    private readonly AutoResetEvent _waitHandle = new AutoResetEvent(false);
    private bool _isStopping;
    private bool _isStarted;
    private bool _isBusy;
    private readonly string _name;
    private ThreadPriority _priority = ThreadPriority.Normal;
    private bool _isBackgroundThread = true;
    private TimeSpan _internalInterval = TimeSpan.FromMilliseconds(500);
    private int _numberOfThreads = 1;
    #endregion
    #region Properties
    protected Thread[] WorkerThreads
    {
      get { return _workerThreads; }
    }


    public int NumberOfThreads
    {
      get { return _numberOfThreads; }
      protected set
      {
        if (_isStarted)
          throw new InvalidOperationException("Cannot change the number of threads while the worker is running.");
        _numberOfThreads = value;
      }
    }


    protected TimeSpan InternalInterval
    {
      get { return _internalInterval; }
      set
      {
        if (_isStarted)
          throw new InvalidOperationException("Cannot change interval while the worker has started. Stop the worker to change the priority.");

        _internalInterval = value;
      }
    }

    public bool IsBackgroundThread
    {
      get { return _isBackgroundThread; }
      set
      {
        if (_isStarted)
          throw new InvalidOperationException("Cannot change thread background property while the worker has started. Stop the worker to change the priority.");

        _isBackgroundThread = value;
      }
    }


    public ThreadPriority Priority
    {
      get { return _priority; }
      set
      {
        if (_isStarted)
          throw new InvalidOperationException("Cannot change thread priority while the worker has started. Stop the worker to change the priority.");

        _priority = value;
      }
    }
    
    public string Name
    {
      get { return _name; }
    }

    protected AutoResetEvent WaitHandle
    {
      get { return _waitHandle; }
    }

    public bool IsStopping
    {
      get { return _isStopping; }
      protected set { _isStopping = value; }
    }

    public bool IsStarted
    {
      get { return _isStarted; }
      protected set { _isStarted = value; }
    }

    public bool IsBusy
    {
      get { return _isBusy; }
      protected set { _isBusy = value; }
    }

    #endregion
    #region Abstract Declarations
    /// <summary>
    /// Override to stop the current operation.
    /// </summary>
    protected abstract void CancelWork();

    /// <summary>
    /// Override to perform the work
    /// </summary>
    protected abstract void PerformWork();
    #endregion
    #region Constructors
    protected Worker(string name)
    {
      #region Validation
      if (name == null)
        throw new ArgumentNullException("name");
      #endregion
      _name = name;
    }
    #endregion

    #region Methods
    public  bool Start()
    {
      #region Validation
      if (_internalInterval== TimeSpan.Zero)
        throw new InvalidOperationException("Interval must be greater than or equal to zero");
      if (_internalInterval.TotalMilliseconds <= 0)
        throw new InvalidOperationException("Interval must be a positive number");
      if (_isStarted)
        throw new InvalidOperationException("Cannot start worker: worker already started");
      #endregion
      if (!OnStarting())
        return false;

      //Set here so no one can call Start again (save us having to declare and manage _isStarting)
      _isStarted = true;

      _workerThreads = new Thread[_numberOfThreads];
      for (int i = 0; i < _numberOfThreads; i++)
      {
        Thread worker = new Thread(WorkerLoop);
        worker.Name = String.Format("Worker: '{0}' (#{1})", _name, i);
        worker.Priority = _priority;
        worker.IsBackground = _isBackgroundThread;
        worker.Start();

        _workerThreads[i] = worker;
      }

      OnStarted();
      return true;
    }

    public bool Stop()
    {
      return Stop(TimeSpan.FromSeconds(10), true);
    }

    public virtual bool Stop(TimeSpan timeout, bool kill)
    {
      #region Validation
      if (!_isStarted)
        throw new InvalidOperationException("Cannot stop worker: worker has not started.");
      if (_isStopping)
        throw new InvalidOperationException("Cannot stop worker: worker is stopping.");
      #endregion
      if (!OnStopping())
        return false;

      _isStopping = true;

      try
      {
        if (_isBusy && kill)
          CancelWork();

        foreach (Thread worker in _workerThreads)
        {
          if (!worker.Join(timeout))
          {
            if (kill)
              worker.Abort();
            else
              OnError(new Exception(String.Format("Unable to stop worker: {0}", _name)));
          }
        }

        _isStarted = false;
        OnStopped();
      }
      //Don't catch exception here. This happens on calling thread outside of the worker; 
      //OnError is in the context if the worker thread. We should bubble STOP exceptions 
      //to the calling thread.
      finally
      {
        _isStopping = false;
      }

      return true;
    }

    protected virtual bool CanPerformWork
    {
      get { return false; }
    }

    protected virtual void WorkerLoop()
    {
      while(!_isStopping)
      {
        bool timedout = !_waitHandle.WaitOne(_internalInterval, false);

        if (_isStopping) 
          return;

        if (timedout && !CanPerformWork)
          continue;

        _isBusy = true;

        try
        {
          if (!OnPerformingWork())
            continue;
          DateTime started = DateTime.Now;
          PerformWork();
          OnWorkPerformed(DateTime.Now - started);
        }
        catch (Exception ex)
        {
          if (!OnError(ex))
            return;
        }
        finally
        {
          _isBusy = false;
        }
      }
    }
    #endregion

    #region Events

    public event System.ComponentModel.CancelEventHandler Starting;

    ///<returns>False if the event is cancelled</returns>
    protected bool OnStarting()
    {
      System.ComponentModel.CancelEventArgs e = new System.ComponentModel.CancelEventArgs();
      OnStarting(e);
      return !e.Cancel;
    }

    protected virtual void OnStarting(System.ComponentModel.CancelEventArgs e)
    {
      if (Starting != null)
        Starting(this, e);
    }


    public event EventHandler Started;

    protected void OnStarted()
    {
      OnStarted(EventArgs.Empty);
    }

    protected virtual void OnStarted(EventArgs e)
    {
      if (Started != null)
        Started(this, e);
    }


    public event System.ComponentModel.CancelEventHandler Stopping;

    ///<returns>False if the event is cancelled</returns>
    protected bool OnStopping()
    {
      System.ComponentModel.CancelEventArgs e = new System.ComponentModel.CancelEventArgs();
      OnStopping(e);
      return !e.Cancel;
    }

    protected virtual void OnStopping(System.ComponentModel.CancelEventArgs e)
    {
      if (Stopping != null)
        Stopping(this, e);
    }


    public event EventHandler Stopped;

    protected void OnStopped()
    {
      OnStopped(EventArgs.Empty);
    }

    protected virtual void OnStopped(EventArgs e)
    {
      if (Stopped != null)
        Stopped(this, e);
    }


    #region ErrorEventArgs Subclass
    public class ErrorEventArgs : CancelEventArgs
    {
      #region Fields
      private readonly Exception _exception;
      #endregion
      #region Properties
      public Exception Exception
      {
        get { return _exception; }
      }
      #endregion
      #region Constructors
      internal ErrorEventArgs(Exception exception)
      {
        #region Validation
        if (exception == null)
          throw new ArgumentNullException("exception");
        #endregion
        _exception = exception;
      }
      #endregion
    }
    #endregion
    
    public event EventHandler<ErrorEventArgs> Error;

    protected bool OnError(Exception exception)
    {
      return OnError(new ErrorEventArgs(exception));
    }

    protected virtual bool OnError(ErrorEventArgs e)
    {
      if (Error != null)
        Error(this, e);
      return !e.Cancel;
    }


    #region WorkPerformedEventArgs Subclass
    public class WorkPerformedEventArgs : EventArgs
    {
      #region Fields
      private readonly TimeSpan _duration;
      #endregion
      #region Properties
      public TimeSpan Duration
      {
        get { return _duration; }
      }
      #endregion
      #region Constructors
      internal WorkPerformedEventArgs(TimeSpan duration)
      {
        #region Validation
        if (duration == null)
          throw new ArgumentNullException("duration");
        #endregion
        _duration = duration;
      }
      #endregion
    }
    #endregion

    public event EventHandler<WorkPerformedEventArgs> WorkPerformed;

    protected void OnWorkPerformed(TimeSpan duration)
    {
      OnWorkPerformed(new WorkPerformedEventArgs(duration));
    }

    protected virtual void OnWorkPerformed(WorkPerformedEventArgs e)
    {
      if (WorkPerformed != null)
        WorkPerformed(this, e);
    }


    public event CancelEventHandler PerformingWork;

    ///<returns>False if the event is cancelled</returns>
    protected bool OnPerformingWork()
    {
      CancelEventArgs e = new CancelEventArgs();
      OnPerformingWork(e);
      return !e.Cancel;
    }

    protected virtual void OnPerformingWork(CancelEventArgs e)
    {
      if (PerformingWork != null)
        PerformingWork(this, e);
    }



    #endregion

  }
}
